package org.apache.solr.handler;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.lucene.index.IndexCommit;
import org.apache.lucene.store.Lock;
import org.apache.lucene.store.SimpleFSLockFactory;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.core.IndexDeletionPolicyWrapper;
import org.apache.solr.core.SolrCore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p/> Provides functionality equivalent to the snapshooter script </p>
 * @since solr 1.4
 */
public class SnapShooter {

    private static final Logger LOG = LoggerFactory.getLogger(SnapShooter.class.getName());
    private String snapDir = null;
    private SolrCore solrCore;
    private SimpleFSLockFactory lockFactory;

    public SnapShooter(SolrCore core, String location) {

        solrCore = core;
        if(location == null) {
            snapDir = core.getDataDir();
        }
        else {
            File base = new File(core.getCoreDescriptor().getInstanceDir());
            snapDir = org.apache.solr.util.FileUtils.resolvePath(base, location).getAbsolutePath();
            File dir = new File(snapDir);
            if(!dir.exists()) {
                dir.mkdirs();
            }
        }
        lockFactory = new SimpleFSLockFactory(snapDir);
    }

    void createSnapAsync(final IndexCommit indexCommit, final ReplicationHandler replicationHandler) {
        createSnapAsync(indexCommit, Integer.MAX_VALUE, replicationHandler);
    }

    void createSnapAsync(final IndexCommit indexCommit, final int numberToKeep, final ReplicationHandler replicationHandler) {
        replicationHandler.core.getDeletionPolicy().saveCommitPoint(indexCommit.getGeneration());

        new Thread() {
            @Override
            public void run() {
                createSnapshot(indexCommit, numberToKeep, replicationHandler);
            }
        }.start();
    }

    void createSnapshot(final IndexCommit indexCommit, int numberToKeep, ReplicationHandler replicationHandler) {

        NamedList<Object> details = new NamedList<>();
        details.add("startTime", new Date().toString());
        File snapShotDir = null;
        String directoryName = null;
        Lock lock = null;
        try {
            if(numberToKeep < Integer.MAX_VALUE) {
                deleteOldBackups(numberToKeep);
            }
            SimpleDateFormat fmt = new SimpleDateFormat(DATE_FMT, Locale.ROOT);
            directoryName = "snapshot." + fmt.format(new Date());
            lock = lockFactory.makeLock(directoryName + ".lock");
            if(lock.isLocked()) {
                return;
            }
            snapShotDir = new File(snapDir, directoryName);
            if(!snapShotDir.mkdir()) {
                LOG.warn("Unable to create snapshot directory: " + snapShotDir.getAbsolutePath());
                return;
            }
            Collection<String> files = indexCommit.getFileNames();
            FileCopier fileCopier = new FileCopier(solrCore.getDeletionPolicy(), indexCommit);
            fileCopier.copyFiles(files, snapShotDir);

            details.add("fileCount", files.size());
            details.add("status", "success");
            details.add("snapshotCompletedAt", new Date().toString());
        }
        catch(Exception e) {
            SnapPuller.delTree(snapShotDir);
            LOG.error("Exception while creating snapshot", e);
            details.add("snapShootException", e.getMessage());
        }
        finally {
            replicationHandler.core.getDeletionPolicy().releaseCommitPoint(indexCommit.getGeneration());
            replicationHandler.snapShootDetails = details;
            if(lock != null) {
                try {
                    lock.release();
                }
                catch(IOException e) {
                    LOG.error("Unable to release snapshoot lock: " + directoryName + ".lock");
                }
            }
        }
    }

    private void deleteOldBackups(int numberToKeep) {

        File[] files = new File(snapDir).listFiles();
        List<OldBackupDirectory> dirs = new ArrayList<>();
        for(File f : files) {
            OldBackupDirectory obd = new OldBackupDirectory(f);
            if(obd.dir != null) {
                dirs.add(obd);
            }
        }
        Collections.sort(dirs);
        int i = 1;
        for(OldBackupDirectory dir : dirs) {
            if(i++ > numberToKeep - 1) {
                SnapPuller.delTree(dir.dir);
            }
        }
    }

    private class OldBackupDirectory implements Comparable<OldBackupDirectory> {

        File dir;
        Date timestamp;
        final Pattern dirNamePattern = Pattern.compile("^snapshot[.](.*)$");

        OldBackupDirectory(File dir) {
            if(dir.isDirectory()) {
                Matcher m = dirNamePattern.matcher(dir.getName());
                if(m.find()) {
                    try {
                        this.dir = dir;
                        this.timestamp = new SimpleDateFormat(DATE_FMT, Locale.ROOT).parse(m.group(1));
                    }
                    catch(Exception e) {
                        this.dir = null;
                        this.timestamp = null;
                    }
                }
            }
        }

        @Override
        public int compareTo(OldBackupDirectory that) {
            return that.timestamp.compareTo(this.timestamp);
        }
    }
    public static final String SNAP_DIR = "snapDir";
    public static final String DATE_FMT = "yyyyMMddHHmmssSSS";

    private class FileCopier {

        private static final int DEFAULT_BUFFER_SIZE = 32768;
        private byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        private IndexCommit indexCommit;
        private IndexDeletionPolicyWrapper delPolicy;

        public FileCopier(IndexDeletionPolicyWrapper delPolicy, IndexCommit commit) {
            this.delPolicy = delPolicy;
            this.indexCommit = commit;
        }

        public void copyFiles(Collection<String> files, File destDir) throws IOException {

            for(String indexFile : files) {
                File source = new File(solrCore.getIndexDir(), indexFile);
                copyFile(source, new File(destDir, source.getName()), true);
            }
        }

        public void copyFile(File source, File destination, boolean preserveFileDate) throws IOException {
            // check source exists
            if(!source.exists()) {
                String message = "File " + source + " does not exist";
                throw new FileNotFoundException(message);
            }

            // does destinations directory exist ?
            if(destination.getParentFile() != null && !destination.getParentFile().exists()) {
                destination.getParentFile().mkdirs();
            }

            // make sure we can write to destination
            if(destination.exists() && !destination.canWrite()) {
                String message = "Unable to open file " + destination + " for writing.";
                throw new IOException(message);
            }

            FileInputStream input = null;
            FileOutputStream output = null;
            try {
                input = new FileInputStream(source);
                output = new FileOutputStream(destination);

                int count = 0;
                int n = 0;
                int rcnt = 0;
                while(-1 != (n = input.read(buffer))) {
                    output.write(buffer, 0, n);
                    count += n;
                    rcnt++;
                    /**
                     * *
                     * // reserve every 4.6875 MB
                     * if (rcnt == 150) {
                     * rcnt = 0;
                     * delPolicy.setReserveDuration(indexCommit.getVersion(), reserveTime);
                     * }
                     **
                     */
                }
            }
            finally {
                try {
                    IOUtils.closeQuietly(input);
                }
                finally {
                    IOUtils.closeQuietly(output);
                }
            }

            if(source.length() != destination.length()) {
                String message = "Failed to copy full contents from " + source + " to " + destination;
                throw new IOException(message);
            }

            if(preserveFileDate) {
                // file copy should preserve file date
                destination.setLastModified(source.lastModified());
            }
        }
    }
}
